package jssdsync;


import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * @author dmitry.mamonov
 */
public class JSSDSyncMain {
    @SuppressWarnings({"PointlessBooleanExpression", "UnusedDeclaration"})
    private static void incrementRunCounter() throws IOException {
        File runCounter = new File("runcounter.txt");
        if (runCounter.exists() == false) {
            FileUtils.writeStringToFile(runCounter, "1");
        } else {
            String countAsStr = FileUtils.readFileToString(runCounter);
            int tryNumber = Integer.parseInt(countAsStr) + 1;
            System.out.println("try No: " + tryNumber);
            FileUtils.writeStringToFile(runCounter, "" + tryNumber);
        }
    }

    public static volatile boolean debug = false;
    public static volatile boolean trace = false;
    public static volatile boolean changes = false;
    public static final long start = System.currentTimeMillis();
    public static int concurrencyLevel = 8;

    public static void printHelp() {
        System.out.println("jssdsync - directories synchronization tool.");
        System.out.println("");
        System.out.println("ATTENTION: this tool is designed to work on storage with good parallel");
        System.out.println("reading and writing support. Like Solid State Drives. You might notice");
        System.out.println("poor performance of this tool on classic HDD storage. Use rsync in this");
        System.out.println("case instead.");
        System.out.println("");
        System.out.println("What it does: tool updates target directory to make it contents same to source directory.");
        System.out.println("Required options:");
        System.out.println("  --source - path so source directory or to ZIP archive. (readonly)");
        System.out.println("  --target - path to target directory (which content will be made exactly same as in source directory). (read/write)");
        System.out.println("Concurrency options (optional):");
        System.out.println("  --parallel - number os simultaneously running concurrent threads (" + concurrencyLevel + " by default).");
        System.out.println("Logging options (optional):");
        System.out.println("  --changes - print all changes made in file system.");
        System.out.println("  --trace - print processing flow information, e.g. currently scanned directories.");
        System.out.println("  --debug - print debug information about concurrent execution.");
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    public static void main(String[] args) throws IOException {
        if (args.length == 0) {
            printHelp();
            System.exit(0);
        }
        File sourceDirParam = null;
        File targetDirParam = null;
        try {
            String prevParam = null;
            for (String param : args) {
                if ("--help".equals(param) || "-h".equals(param)) {
                    printHelp();
                    System.exit(0);
                } else if ("--trace".equals(param)) {
                    trace = true;
                } else if ("--debug".equals(param)) {
                    debug = true;
                } else if ("--changes".equals(param)) {
                    changes = true;
                } else {
                    if (prevParam == null) {
                        prevParam = param;
                    } else {
                        if (prevParam.equals("--source")) {
                            sourceDirParam = new File(param);
                            prevParam = null;
                        } else if (prevParam.equals("--target")) {
                            targetDirParam = new File(param);
                            prevParam = null;
                        } else if (prevParam.equals("--parallel")) {
                            concurrencyLevel = Integer.parseInt(param);
                            prevParam = null;
                        } else {
                            throw new IllegalArgumentException("Unknown param: " + prevParam);
                        }
                    }
                }
            }
            if (prevParam != null) {
                throw new IllegalArgumentException("Unknown param: " + prevParam);
            }

            if (sourceDirParam == null) {
                throw new IllegalArgumentException("--source is not specified");
            }
            if (sourceDirParam.exists() == false) {
                throw new IllegalArgumentException("Source path is not exists: " + sourceDirParam.getAbsolutePath());
            }
            if (targetDirParam == null) {
                throw new IllegalArgumentException("--target is not specified");
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            printHelp();
            System.exit(1);
        }

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("Completed in: " + ((System.currentTimeMillis() - start) / 1000) + "sec");
            }
        });

        final File sourceDir = sourceDirParam;
        final File targetDir = targetDirParam;

        //pipeline:
        final AtomicReference<FileAdder> refAdderDir = new AtomicReference<FileAdder>();
        final AtomicReference<FileDeleter> refDeleterFile = new AtomicReference<FileDeleter>();
        final Runnable scanStageFinishedHandler = new Runnable() {
            int countDown = 2;

            public synchronized void run() {
                countDown--;
                if (countDown == 0) {
                    refAdderDir.get().startRace(1);
                    refDeleterFile.get().startRace(concurrencyLevel);
                }
            }
        };

        final IOChannelImpl<FileWrapper> sourceDirScannerChannel = new IOChannelImpl<FileWrapper>(scanStageFinishedHandler);
        final IOChannelImpl<FileWrapper> targetDirScannerChannel = new IOChannelImpl<FileWrapper>(scanStageFinishedHandler);
        final Producer<FileWrapper> inputScanner;
        if (sourceDir.isDirectory()) {
            inputScanner = new DirectoryScanner(sourceDirScannerChannel, sourceDir);
        } else {
            inputScanner = new ZipScanner(sourceDirScannerChannel, sourceDir);
        }
        final DirectoryScanner outputScanner = new DirectoryScanner(targetDirScannerChannel, targetDir);
        final Merger merger = new Merger(sourceDirScannerChannel, targetDirScannerChannel);
        final FileUpdater fileUpdater = new FileUpdater(merger.getFileUpdateChannel(), sourceDir, targetDir);
        refAdderDir.set(new FileAdder(merger.getFileAddDirChannel(), sourceDir, targetDir, new Runnable() {
            @Override
            public void run() {
                new FileAdder(merger.getFileAddFileChannel(), sourceDir, targetDir, null).startRace(concurrencyLevel);
            }
        }));
        refDeleterFile.set(new FileDeleter(merger.getFileDeleteFileChannel(), sourceDir, targetDir, new Runnable() {
            @Override
            public void run() {
                new FileDeleter(merger.getFileDeleteDirChannel(), sourceDir, targetDir, null).startRace(1);
            }
        }));

        //start threads when everything is configured:
        inputScanner.startSingleton();
        outputScanner.startSingleton();
        merger.startSingleton();
        fileUpdater.startRace(concurrencyLevel);
    }
}

interface OutputChannel<MSG> {
    void put(MSG out);

    void finished();
}

interface InputChannel<MSG> {
    MSG take();

    boolean isFinished();
}

interface IOChannel<MSG> extends OutputChannel<MSG>, InputChannel<MSG> {
}

class IOChannelImpl<MSG> implements IOChannel<MSG> {
    private BlockingQueue<MSG> queue;
    private Runnable finishCallback;
    private volatile boolean finished = false;

    IOChannelImpl(Runnable finishCallback) {
        this(new ArrayBlockingQueue<MSG>(100000), finishCallback);
    }

    private IOChannelImpl(BlockingQueue<MSG> queue, Runnable finishCallback) {
        this.queue = queue;
        this.finishCallback = finishCallback != null ? finishCallback : new Runnable() {
            public void run() {
                //ignore.
            }
        };
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    public void finished() {
        if (finished == true) {
            throw new IllegalStateException();
        } else {
            finished = true;
        }
        finishCallback.run();
    }

    public void put(MSG out) {
        try {
            queue.put(out);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public MSG take() {
        return queue.poll();
    }

    public boolean isFinished() {
        return finished;
    }
}

final class FileWrapper {
    String relativePath;
    boolean directory;
    byte[] sourceBytes = null;

    FileWrapper(String relativeName, boolean directory) {
        this.relativePath = relativeName.replace('\\','/');
        if (this.relativePath.startsWith("/")){
            this.relativePath=this.relativePath.substring(1);
        }
        this.directory = directory;
    }

    public String getRelativePath() {
        return relativePath;
    }

    public boolean isDirectory() {
        return directory;
    }

    public byte[] getSourceBytes() {
        return sourceBytes;
    }

    public void setSourceBytes(byte[] sourceBytes) {
        this.sourceBytes = sourceBytes;
    }

    @SuppressWarnings({"RedundantIfStatement"})
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        FileWrapper that = (FileWrapper) o;

        if (relativePath != null ? !relativePath.equals(that.relativePath) : that.relativePath != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return relativePath != null ? relativePath.hashCode() : 0;
    }
}

class ErrorHandlingThread extends Thread {
    ErrorHandlingThread(Runnable target, String name) {
        super(target, name);
        setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.print("THREAD " + t.getName() + ": ");
                e.printStackTrace();
                System.exit(1);
            }
        });
    }
}

abstract class Consumer<MSG> implements Runnable {
    protected final InputChannel<MSG> inputChannel;
    protected final Runnable callbackWhenDone;

    Consumer(InputChannel<MSG> inputChannel, Runnable callbackWhenDone) {
        this.inputChannel = inputChannel;
        this.callbackWhenDone = callbackWhenDone != null ? callbackWhenDone : new Runnable() {
            @Override
            public void run() {
                //ignore.
            }
        };
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    public final void run() {
        while (true) {
            final MSG input = inputChannel.take();
            if (input != null) {
                processInput(input);
            } else {
                if (inputChannel.isFinished()) {
                    break;
                } else {
                    Thread.yield();
                }
            }
        }
        whenFinished();
        callbackWhenDone.run();
    }

    protected abstract void processInput(MSG input);

    protected abstract void whenFinished();

    public void startRace(final int horses) {
        for (int horse = 0; horse < horses; horse++) {
            String horseName = this.getClass().getSimpleName() + "-horse-no-" + horse;
            if (JSSDSyncMain.debug) System.out.println("go " + horseName);
            new ErrorHandlingThread(this, horseName).start();
        }
    }
}

abstract class Producer<MSG> implements Runnable {
    protected final OutputChannel<MSG> outputChannel;
    private boolean started = false;

    protected Producer(OutputChannel<MSG> outputChannel) {
        this.outputChannel = outputChannel;
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    public void startSingleton() {
        if (started == true) {
            throw new IllegalStateException("Already started.");
        } else {
            started = true;
            String producerName = this.getClass().getSimpleName();
            if (JSSDSyncMain.debug) System.out.println("go " + producerName);
            new ErrorHandlingThread(this, producerName).start();
        }
    }
}

class ZipScanner extends Producer<FileWrapper> {
    File baseFile;
    ConcurrentMap<FileWrapper, Boolean> alreadyProcessedSet = new ConcurrentHashMap<FileWrapper, Boolean>();

    ZipScanner(final OutputChannel<FileWrapper> outputChannel, File baseFile) throws IOException {
        super(new OutputChannel<FileWrapper>() {
            @Override
            public void put(FileWrapper out) {
                outputChannel.put(out);
            }

            int pending = JSSDSyncMain.concurrencyLevel;

            @Override
            public synchronized void finished() {
                pending--;
                if (pending == 0) {
                    if (JSSDSyncMain.debug) System.out.println("zip scanner all finished");
                    outputChannel.finished();
                } else if (pending < 0) {
                    throw new IllegalStateException();
                } else {
                    if (JSSDSyncMain.debug) System.out.println("pending zip scanners: " + pending);
                }
            }
        });
        if (baseFile.isFile() == false) {
            throw new IOException("Not a file: " + baseFile.getAbsolutePath());
        }
        this.baseFile = baseFile;
    }

    class NestedZipScanner extends Producer<FileWrapper> {
        NestedZipScanner() {
            super(ZipScanner.this.outputChannel);
        }

        @SuppressWarnings({"PointlessBooleanExpression"})
        @Override
        public void run() {
            ZipFile zipFile = null;
            try {
                zipFile = new ZipFile(baseFile);
                Enumeration<? extends ZipEntry> entries = zipFile.entries();
                while (entries.hasMoreElements()) {
                    ZipEntry zipEntry = entries.nextElement();
                    if (JSSDSyncMain.debug) System.out.println("zip: " + zipEntry.getName());
                    if (zipEntry != null) {
                        FileWrapper file = new FileWrapper(zipEntry.getName().replace('\\', '/'), zipEntry.isDirectory());
                        if (alreadyProcessedSet.putIfAbsent(file, Boolean.TRUE) == null) {
                            if (file.isDirectory() == false) {
                                InputStream zipInput = zipFile.getInputStream(zipEntry);
                                try {
                                    file.setSourceBytes(IOUtils.toByteArray(zipInput));
                                } finally {
                                    IOUtils.closeQuietly(zipInput);
                                }
                                outputChannel.put(file);
                            }
                        }
                    }
                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    zipFile.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (JSSDSyncMain.debug) System.out.println("ZipScanner finished");
            outputChannel.finished();
        }
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    @Override
    public void run() {
        ZipFile zipFile = null;
        try {
            Set<String> pathSet = new LinkedHashSet<String>();

            zipFile = new ZipFile(baseFile);
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                if (JSSDSyncMain.debug) System.out.println("zip: " + zipEntry.getName());
                if (zipEntry != null && zipEntry.isDirectory()) {
                    FileWrapper file = new FileWrapper(zipEntry.getName().replace('\\', '/'), zipEntry.isDirectory());
                    {
                        String[] pathElementsList = file.getRelativePath().split("[/]");

                        String parentPath = "";
                        for (String pathElement : pathElementsList) {
                            if (parentPath.isEmpty() == false) {
                                parentPath += "/";
                            }
                            parentPath += pathElement;
                            pathSet.add(parentPath);
                        }
                    }
                }
            }

            List<String> pathList = new ArrayList<String>(pathSet);
            Collections.sort(pathList, Collections.<Object>reverseOrder());
            for (String path : pathList) {
                FileWrapper dir = new FileWrapper(path, true);
                if (alreadyProcessedSet.putIfAbsent(dir, Boolean.TRUE) == null) {
                    if (JSSDSyncMain.debug) System.out.println("zip create dir: " + dir.getRelativePath());
                    outputChannel.put(dir);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (zipFile != null) {
                try {
                    zipFile.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        for (int i = 0; i < JSSDSyncMain.concurrencyLevel; i++) {
            new NestedZipScanner().startSingleton();
        }
    }
}

class DirectoryScanner extends Producer<FileWrapper> {
    private final File baseDir;
    private final String baseDirPath;

    @SuppressWarnings({"PointlessBooleanExpression"})
    DirectoryScanner(OutputChannel<FileWrapper> outputChannel, File baseDir) throws IOException {
        super(outputChannel);
        this.baseDir = baseDir;
        if (baseDir.isDirectory() == false) {
            if (baseDir.exists()) {
                throw new IOException("Not a directory: " + this.baseDir.getAbsolutePath());
            } else {
                if (this.baseDir.mkdirs() == false) {
                    throw new IOException("Can not create directory: " + this.baseDir.getAbsolutePath());
                }
            }
        }
        this.baseDirPath = this.baseDir.getAbsolutePath();
    }

    @Override
    public void run() {
        processDir(baseDir);
        outputChannel.finished();
    }

    private void processDir(File workingDir) {
        if (JSSDSyncMain.trace) System.out.println("scan: " + workingDir.getAbsolutePath());
        List<FileWrapper> filesToFire = new ArrayList<FileWrapper>();
        for (File entry : workingDir.listFiles()) {
            String relativePath = entry.getAbsolutePath().substring(baseDirPath.length());
            if (entry.isDirectory()) {
                processDir(entry);
                if (JSSDSyncMain.debug) System.out.println("scan dir relative path: " + relativePath);
                outputChannel.put(new FileWrapper(relativePath, true));
            } else {
                filesToFire.add(new FileWrapper(relativePath, false));
            }
        }
        for (FileWrapper file : filesToFire) {
            outputChannel.put(file);
        }
    }
}

class Merger {
    Set<FileWrapper> subjectToAdd = new LinkedHashSet<FileWrapper>();
    Set<FileWrapper> subjectToDelete = new LinkedHashSet<FileWrapper>();
    Set<FileWrapper> sourceFileSet = new HashSet<FileWrapper>();
    Set<FileWrapper> targetFileSet = new HashSet<FileWrapper>();
    InputChannel<FileWrapper> inputSource;
    InputChannel<FileWrapper> inputTarget;
    IOChannelImpl<FileWrapper> outputAddFile = new IOChannelImpl<FileWrapper>(null);
    IOChannelImpl<FileWrapper> outputAddDir = new IOChannelImpl<FileWrapper>(null);
    IOChannelImpl<FileWrapper> outputUpdate = new IOChannelImpl<FileWrapper>(null);
    IOChannelImpl<FileWrapper> outputDeleteFile = new IOChannelImpl<FileWrapper>(null);
    IOChannelImpl<FileWrapper> outputDeleteDir = new IOChannelImpl<FileWrapper>(null);

    protected Merger(InputChannel<FileWrapper> inputSource, InputChannel<FileWrapper> inputTarget) {
        this.inputSource = inputSource;
        this.inputTarget = inputTarget;
    }

    InputChannel<FileWrapper> getFileAddFileChannel() {
        return outputAddFile;
    }

    InputChannel<FileWrapper> getFileAddDirChannel() {
        return outputAddDir;
    }

    InputChannel<FileWrapper> getFileUpdateChannel() {
        return outputUpdate;
    }

    InputChannel<FileWrapper> getFileDeleteFileChannel() {
        return outputDeleteFile;
    }

    InputChannel<FileWrapper> getFileDeleteDirChannel() {
        return outputDeleteFile;
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    protected synchronized void updateSourceSet(FileWrapper file) {
        sourceFileSet.add(file);
        if (subjectToDelete.contains(file)) {
            subjectToDelete.remove(file);
            outputUpdate.put(file);
        } else if (targetFileSet.contains(file) == false) {
            subjectToAdd.add(file);
        } else {
            outputUpdate.put(file);
        }
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    protected synchronized void updateTargetSet(FileWrapper file) {
        targetFileSet.add(file);
        if (subjectToAdd.contains(file)) {
            subjectToAdd.remove(file);
            outputUpdate.put(file);
        } else if (sourceFileSet.contains(file) == false) {
            subjectToDelete.add(file);
        } else {
            outputUpdate.put(file);
        }
    }

    int inputQueuesCountdown = 2;

    protected synchronized void whenInputFinished() {
        inputQueuesCountdown--;
        if (inputQueuesCountdown < 0) {
            throw new IllegalStateException();
        }
        if (inputQueuesCountdown == 0) {
            processAdded();
            processDeleted();
            outputUpdate.finished();
        }
    }

    private void processDeleted() {
        for (FileWrapper file : subjectToDelete) {
            if (file.isDirectory()) {
                outputDeleteDir.put(file);
            } else {
                outputDeleteFile.put(file);
            }
        }
        outputDeleteFile.finished();
        outputDeleteDir.finished();
    }

    private void processAdded() {
        List<FileWrapper> filesToAdd = new ArrayList<FileWrapper>(subjectToAdd);
        Collections.reverse(filesToAdd);
        for (FileWrapper file : filesToAdd) {
            if (file.isDirectory()) {
                outputAddDir.put(file);
            } else {
                outputAddFile.put(file);
            }
        }
        outputAddDir.finished();
        outputAddFile.finished();
    }

    private class SourceConsumer extends Consumer<FileWrapper> {
        private SourceConsumer() {
            super(inputSource, null);
        }

        @Override
        protected void processInput(FileWrapper input) {
            updateSourceSet(input);
        }

        @Override
        protected void whenFinished() {
            whenInputFinished();
        }
    }

    private class TargetConsumer extends Consumer<FileWrapper> {
        TargetConsumer() {
            super(inputTarget, null);
        }

        @Override
        protected void processInput(FileWrapper input) {
            updateTargetSet(input);
        }

        @Override
        protected void whenFinished() {
            whenInputFinished();
        }
    }


    public void startSingleton() {
        new SourceConsumer().startRace(1);
        new TargetConsumer().startRace(1);

    }
}

abstract class FileWorker extends Consumer<FileWrapper> {
    File sourceDir;
    File targetDir;

    FileWorker(InputChannel<FileWrapper> inputChannel, File sourceDir, File targetDir, Runnable callbackWhenDone) {
        super(inputChannel, callbackWhenDone);
        this.sourceDir = sourceDir;
        this.targetDir = targetDir;
    }

    @Override
    protected void whenFinished() {
        //ignore.
    }
}

class FileAdder extends FileWorker {

    FileAdder(InputChannel<FileWrapper> inputChannel, File sourceDir, File targetDir, Runnable callbackWhenDone) {
        super(inputChannel, sourceDir, targetDir, callbackWhenDone);
        this.sourceDir = sourceDir;
        this.targetDir = targetDir;
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    @Override
    protected void processInput(FileWrapper file) {
        File targetFile = new File(targetDir, file.getRelativePath());
        if (file.isDirectory()) {
            if (JSSDSyncMain.changes) System.out.println("add directory: " + file.getRelativePath());
            if (targetFile.mkdir() == false) {
                throw new RuntimeException("Failed to create target directory: " + file.getRelativePath());
            }
        } else {
            if (JSSDSyncMain.changes) System.out.println("add file: " + file.getRelativePath());
            try {
                if (file.getSourceBytes() != null) {
                    FileUtils.writeByteArrayToFile(targetFile, file.getSourceBytes());
                } else {
                    final File sourceFile = new File(sourceDir, file.getRelativePath());
                    FileUtils.copyFile(sourceFile, targetFile);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class FileUpdater extends FileWorker {

    FileUpdater(InputChannel<FileWrapper> inputChannel, File sourceDir, File targetDir) {
        super(inputChannel, sourceDir, targetDir, null);
        this.sourceDir = sourceDir;
        this.targetDir = targetDir;
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    @Override
    protected void processInput(FileWrapper file) {
        if (file.isDirectory()) {
            if (JSSDSyncMain.trace) System.out.println("update directory: " + file.getRelativePath());
        } else {
            try {
                File sourceFile = new File(sourceDir, file.getRelativePath());
                File targetFile = new File(targetDir, file.getRelativePath());
                byte[] sourceBytes = file.getSourceBytes() != null
                        ? file.getSourceBytes()
                        : FileUtils.readFileToByteArray(sourceFile);
                byte[] targetBytes = FileUtils.readFileToByteArray(targetFile);
                if (Arrays.equals(sourceBytes, targetBytes)) {
                    if (JSSDSyncMain.trace) System.out.println("update same file: " + file.getRelativePath());
                } else {
                    if (JSSDSyncMain.changes) System.out.println("update copy file: " + file.getRelativePath());
                    FileUtils.writeByteArrayToFile(targetFile, sourceBytes);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

}


class FileDeleter extends FileWorker {

    FileDeleter(InputChannel<FileWrapper> inputChannel, File sourceDir, File targetDir, Runnable callbackWhenDone) {
        super(inputChannel, sourceDir, targetDir, callbackWhenDone);
        this.sourceDir = sourceDir;
        this.targetDir = targetDir;
    }

    @SuppressWarnings({"PointlessBooleanExpression"})
    @Override
    protected void processInput(FileWrapper file) {
        File targetFileToDelete = new File(targetDir, file.getRelativePath());
        if (JSSDSyncMain.changes) System.out.println("delete: " + file.getRelativePath());
        if (targetFileToDelete.delete() == false) {
            throw new RuntimeException("Can not delete file: " + file.getRelativePath());
        }
    }
}

